<?php

/**
 *
 * @desc This class provides access to database tables
 * @see http://code.google.com/p/dabase
 * @author Barbushin Sergey http://www.linkedin.com/in/barbushin
 *
 */
class DaBase_Collection {
	
	const table = null;
	const objectsClass = null;
	const handlePropertiesOnRead = false;
	const handlePropertiesOnWrite = false;
	
	/**
	 * @var DaBase_Connection
	 */
	protected $db;
	/**
	 * @var DaBase_Object
	 */
	protected $objectsClass;
	
	protected $alias;
	protected $table;
	protected $limit;
	protected $offset;
	protected $orderBy;
	protected $groupBy;
	protected $savedStatuses;
	protected $whereSubstrings;
	protected $whereLastString;
	protected static $whereSpecialStrings = array('OR', 'XOR', 'AND', '(', ')');
	protected static $statusProperties = array('whereSubstrings', 'whereLastString', 'limit', 'offset', 'orderBy', 'appenders');
	
	protected $appenders;

	public function __construct(DaBase_Connection $db, $alias, $table, $objectsClass) {
		$this->db = $db;
		if(!$table || !$objectsClass) {
			throw new Exception();
		}
		$this->table = $table;
		$this->alias = $alias;
		$this->objectsClass = $objectsClass;
		$this->postInit();
	}

	protected function postInit() {
	}

	public function getDb() {
		return $this->db;
	}

	public function getTable() {
		return $this->table;
	}

	public function getAlias() {
		return $this->alias;
	}

	/**************************************************************
	 PROPERTIES DATA HANDLING
	 **************************************************************/
	
	protected function handlePropertiesOnWrite(array $properties, &$conditionOperand = null) {
		return $properties;
	}

	protected function handlePropertiesOnRead(array $row) {
		return $row;
	}

	/**************************************************************
	 COLLECTION CRUD
	 **************************************************************/
	
	public function getNew(array $properties = array()) {
		return new $this->objectsClass($properties);
	}

	public function get($oneObject = false) {
		$result = $this->getByPreparedQuery($this->db->sql('SELECT * FROM ' . $this->db->quoteName($this->table) . $this->sqlWhere() . $this->sqlGroupBy() . $this->sqlOrderBy() . $this->sqlLimit()), $oneObject);
		$this->resetFilter();
		return $result;
	}

	public function getByQuery($prepareSql) {
		$replacers = array_slice(func_get_args(), 1);
		return $this->getByPreparedQuery($this->db->prepareSql($prepareSql, $replacers));
	}

	public function getOneByQuery($prepareSql) {
		$replacers = array_slice(func_get_args(), 1);
		return $this->getByPreparedQuery($this->db->prepareSql($prepareSql, $replacers), true);
	}

	protected function getByPreparedQuery($preparedSql, $oneObject = false) {
		$objects = array();
		foreach($this->db->fetchPreparedSql($preparedSql) as $i => $row) {
			if(static::handlePropertiesOnRead) {
				$row = $this->handlePropertiesOnRead($row);
			}
			$objects[isset($row['id']) ? $row['id'] : $i] = $this->getNew($row);
		}
		
		$this->handleAppendersToObjects($objects);
		
		if($oneObject) {
			if(!$objects) {
				return null;
			}
			elseif(count($objects) > 1) {
				throw new DaBase_Exception('Request returns more than one object');
			}
			return reset($objects);
		}
		return $objects;
	}

	public function getObjectById($id, $throwExcptionIfNotFound = true) {
		$this->saveStatus();
		$objects = $this->addWhereCondition('id', $id)->get();
		$this->loadStatus();
		
		if($objects) {
			return reset($objects);
		}
		elseif($throwExcptionIfNotFound) {
			throw new DaBase_ObjectNotFound($this, $id);
		}
	}

	public function getColumn($valueProperty, $keyProperty = 'id') {
		$founds = array();
		foreach($this->db->fetchPreparedSql('SELECT ' . $this->db->quoteName($keyProperty) . ', ' . $this->db->quoteName($valueProperty) . ' FROM ' . $this->db->quoteName($this->table) . $this->sqlWhere() . $this->sqlOrderBy() . $this->sqlLimit()) as $row) {
			if(static::handlePropertiesOnRead) 

			{
				$row = $this->handlePropertiesOnRead($row);
			}
			$founds[$row[$keyProperty]] = $row[$valueProperty];
		}
		$this->resetFilter();
		return $founds;
	}

	public function getProperty($property) {
		$value = $this->db->fetchPreparedSql('SELECT ' . $this->db->quoteName($property) . ' as `value` FROM ' . $this->db->quoteName($this->table) . $this->sqlWhere() . $this->sqlOrderBy() . $this->sqlLimit(), true, true);
		if(static::handlePropertiesOnRead) 

		{
			list($value) = $this->handlePropertiesOnRead(array($property => $value));
		}
		$this->resetFilter();
		return $value;
	}

	public function count() {
		$count = (int) $this->db->fetchPreparedSql('SELECT COUNT(*) FROM ' . $this->db->quoteName($this->table) . $this->sqlWhere(), true, true);
		$this->resetFilter();
		return $count;
	}

	public function update($propertiesValuesOrObject) {
		if(is_object($propertiesValuesOrObject)) {
			return $this->updateObject($propertiesValuesOrObject);
		}
		return $this->updatePropertiesValues($propertiesValuesOrObject);
	}

	public function insert(DaBase_Object $object, $checkId = true) {
		return $this->insertObject($object, $checkId);
	}

	public function updatePropertiesValues(array $propertiesValues) {
		$object = $this->getNew($propertiesValues);
		$object->validate(array_keys($propertiesValues));
		
		if(static::handlePropertiesOnWrite) {
			$propertiesValues = $this->handlePropertiesOnWrite($propertiesValues);
		}
		$this->db->exec('UPDATE ' . $this->db->quoteName($this->table) . ' SET ' . implode(', ', $this->db->quoteEquals($propertiesValues)) . $this->sqlWhere() . $this->sqlOrderBy() . $this->sqlLimit());
		$this->resetFilter();
		return $this;
	}

	public function delete(DaBase_Object $object = null) {
		if($object) {
			return $this->deleteObject($object);
		}
		return $this->deleteByFilter();
	}

	protected function deleteByFilter() {
		$this->db->exec('DELETE FROM ' . $this->db->quoteName($this->table) . $this->sqlWhere() . $this->sqlOrderBy() . $this->sqlLimit());
		$this->resetFilter();
		return $this;
	}

	/**************************************************************
	 OBJECT CRUD
	 **************************************************************/
	
	public function insertObject(DaBase_Object $object, $checkId = true) {
		if($checkId && isset($object->id)) {
			throw new Exception('Trying to make insert() of object with not empty "id"');
		}
		$object->validate();
		
		$propertiesValues = $object->asArray(true, true);
		if(static::handlePropertiesOnWrite) 

		{
			$propertiesValues = $this->handlePropertiesOnWrite($propertiesValues);
		}
		$this->db->exec($this->db->sqlInsert($this->table, $propertiesValues));
		$object->id = $this->db->getLastInsertId();
		return $object;
	}

	public function updateObject(DaBase_Object $object) {
		if(!$object->id) {
			throw new Exception('Trying to update object with empty "id"');
		}
		$object->validate();
		
		$propertiesValues = $object->asArray(true, true);
		if(static::handlePropertiesOnWrite) 

		{
			$propertiesValues = $this->handlePropertiesOnWrite($propertiesValues);
		}
		$this->db->exec('UPDATE ' . $this->db->quoteName($this->table) . 'SET ' . implode(', ', $this->db->quoteEquals($propertiesValues)) . ' WHERE id=' . $this->db->quote($object->id));
		return $object;
	}

	public function deleteObject(DaBase_Object $object) {
		if(!$object->id) {
			throw new Exception('Trying to delete object with empty "id"');
		}
		$this->db->exec('DELETE FROM ' . $this->db->quoteName($this->table) . ' WHERE id=' . $this->db->quote($object->id));
		$object->id = null;
		return $object;
	}

	/**************************************************************
	 APPENDING
	 **************************************************************/
	
	public function append(DaBase_Collection $collection, $appendProperty = null, $joinChildProperty = null, $joinParentProperty = null, $multiple = true) {
		if(!$appendProperty) {
			$appendProperty = $collection->getAlias();
		}
		if(!$joinChildProperty) {
			$joinChildProperty = $this->db->getRouter()->getJoinFieldNameByCollection($this);
			var_dump($joinChildProperty);
		}
		if(!$joinParentProperty) {
			$joinParentProperty = 'id';
		}
		
		if(isset($this->appenders[$appendProperty])) {
			throw new DaBase_Exception('Property with name "' . $appendProperty . '" already appended');
		}
		$this->appenders[$appendProperty] = array($collection, $appendProperty, $joinChildProperty, $joinParentProperty, $multiple);
		return $this;
	}

	protected function handleAppendersToObjects(array $objects) {
		if($objects && $this->appenders) {
			
			foreach($this->appenders as $appender) {
				
				list($collection, $appendProperty, $joinChildProperty, $joinParentProperty, $multiple) = $appender;
				
				if($collection->getLimit() || $collection->getOffset()) {
					throw new DaBase_Exception('Cannot append collection with not empty limit or offset');
				}
				
				$attachProperty = '_' . $appendProperty;
				$parentPropertyValuesAndObjectsIds = array();
				foreach($objects as $id => $object) {
					$parentPropertyValuesAndObjectsIds[$object->$joinParentProperty][] = $id;
					$object->$attachProperty = $multiple ? array() : null;
				}
				
				$append = $collection->filter($joinChildProperty, array_keys($parentPropertyValuesAndObjectsIds))->get();
				
				foreach($append as $childId => $appendObject) {
					foreach($parentPropertyValuesAndObjectsIds[$appendObject->$joinChildProperty] as $id) {
						if($multiple) {
							$objects[$id]->{$attachProperty}[$childId] = $appendObject;
						}
						else {
							$objects[$id]->$attachProperty = $appendObject;
						}
					}
				}
			}
		}
	}

	/**************************************************************
	 GETTER SETTINGS
	 **************************************************************/
	
	public function resetFilter() {
		foreach(self::$statusProperties as $property) {
			$this->$property = null;
		}
		return $this;
	}

	public function saveStatus() {
		$savedStatus = array();
		foreach(self::$statusProperties as $property) {
			$savedStatus[$property] = $this->$property;
		}
		$this->savedStatuses[] = $savedStatus;
		$this->resetFilter();
	}

	public function loadStatus() {
		foreach(array_pop($this->savedStatuses) as $property => $value) {
			$this->$property = $value;
		}
	}

	protected function addWhereCondition($property, $value, $operand = '=') {
		$operand = trim($operand);
		
		if(static::handlePropertiesOnWrite) 

		{
			$values = $this->handlePropertiesOnWrite(array($property => $value), $operand);
			$value = reset($values);
		}
		
		if($operand == '=' && is_array($value)) {
			$sql = $this->db->quoteName($property) . ' IN (' . implode(',', $this->db->quoteArray($value)) . ')';
			$this->whereSubstrings[] = $value ? $sql : '0';
		}
		elseif($operand == '=' && $value === null) {
			$this->whereSubstrings[] = $this->db->quoteName($property) . ' IS NULL';
		}
		else {
			$this->whereSubstrings[] = $this->db->quoteName($property) . ' ' . $operand . ' ' . $this->db->quote($value);
		}
		
		$this->whereLastString = null;
		return $this;
	}

	public function filter($property, $value, $operand = '=') {
		if(!$this->whereLastString && $this->whereSubstrings) {
			$this->addWhereString('AND');
		}
		$this->addWhereCondition($property, $value, $operand);
		return $this;
	}

	public function addWhereString($string) {
		$this->whereLastString = $string;
		$this->whereSubstrings[] = $string;
		return $this;
	}

	public function sqlWhere() {
		if($this->whereSubstrings) {
			$where = implode(' ', $this->whereSubstrings);
			return ' WHERE ' . $where;
		}
		return '';
	}

	public function orderBy($property, $desc = false) {
		$this->orderBy[] = $this->db->quoteName($property) . ($desc ? ' DESC' : '');
		return $this;
	}

	public function orderByRand() {
		$this->orderBy[] = $this->db->sqlRandomFunction();
		return $this;
	}

	protected function sqlOrderBy() {
		if($this->orderBy) {
			return ' ORDER BY ' . implode(', ', $this->orderBy);
		}
		return '';
	}

	public function groupBy($property, $desc = false) {
		$this->groupBy[] = $this->db->quoteName($property) . ($desc ? ' DESC' : '');
		return $this;
	}

	protected function sqlGroupBy() {
		if($this->groupBy) {
			return ' GROUP BY ' . implode(', ', $this->groupBy);
		}
		return '';
	}

	public function limit($limit, $offset = null) {
		$this->limit = $limit;
		$this->offset = $offset;
		return $this;
	}

	public function limitPage($itemsOnPage, $page = 1) {
		return $this->limit($itemsOnPage, $itemsOnPage * ($page - 1));
	}

	public function getLimit() {
		return $this->limit;
	}

	public function getOffset() {
		return $this->offset;
	}

	protected function sqlLimit() {
		if($this->limit || $this->offset) {
			return ' ' . $this->db->sqlLimitOffset($this->limit, $this->offset);
		}
		return '';
	}

	/**************************************************************
	 MAGICS
	 **************************************************************/
	
	public function __toString() {
		return $this->table;
	}

	public function __get($property) {
		if(strstr($property, ' ') || in_array($property, self::$whereSpecialStrings)) {
			$this->addWhereString($property);
			return $this;
		}
		return $this->append($this->db->getRouter()->getCollectionByAlias($property, $this->db));
	}

	public function __set($property, $value) {
		return $this->update(array($property => $value));
	}

	public function __call($method, $attributes) {
		if(!$attributes) {
			throw new DaBase_Exception('Unkown method "' . $method . '" requested');
		}
		return $this->filter($method, $attributes[0], count($attributes) > 1 ? $attributes[1] : '=');
	}

	public function __invoke($value) {
		return $this->getObjectById($value);
	}
}

/**
 *
 * @desc This is exception of unkown objects requests
 * @author Barbushin Sergey http://www.linkedin.com/in/barbushin
 *
 */
class DaBase_ObjectNotFound extends DaBase_Exception {
	
	protected $collection;
	protected $objectId;

	public function __construct(DaBase_Collection $collection, $objectId) {
		$this->collection = $collection;
		$this->objectId = $objectId;
		parent::__construct('Object with id "' . $objectId . '" not found in table "' . $collection->getTable() . '"');
	}
}